import os
import sys

__revision__ = '$Id$'

#
# This script manages the project's dependencies. It can be used by any
# component, and so facilitates component-level builds.
#
# It supports two general schemes. 
# 
# The first (and default) is based on 'fetchDependencies' and assumes all the
# dependencies are in a single location.
#
# The second assumes a single repo was checked out, and all locations are
# assumed to be relative to this.
#
# To switch between these, use the option --useCheckedOutDependencies
#
# There are options to enable flexibilty:
# 
# When using fetch dependencies, the single location can be specifed
#   (--csfDependencyDir=loc)
#
# The Dependency Location File can be specified:
#   (--checkedoutDependencyLocationFile=file.txt)
#   by default 'CheckedOutDependencyLocation.txt'
# 
# Any individual dependency can be specifed:
#   (--csfDependencyDir_<dependency_name>=loc)
#
# The 'root' location for the Checked out locations:
#   (--projectRoot=loc)
#   (by default, the root is the directory where 'patchbuild.py' is found.
# 
# Consuming builds are required to pass in a list of dependencies that they require. 
#
# The script returns 'scripts', which should be added to the python path, and 
# a list of dependenices. These locations are assumed to have a 'SConscript'
# file.
#

class DependencyException(Exception):
    pass

Import('dependencies')
# import test dependencies if defined
try:
    Import('testDependencies')
except:
    testDependencies = None

dependencyPolicyKnown = False
dependencyPolicy = ""

def find_project_root(reference_file = 'patchbuild.py'):
    """
    The project's root directory is found by searching up the 
    directory tree until a well-known file (patchbuild.py) is found. 

    """
    pwd = os.getcwd()
    while pwd != os.path.dirname(pwd):  # at drive root?
        reference_path = os.path.join(pwd, reference_file)
        if os.path.exists(reference_path):
            # Found the file!
            return pwd
        pwd = os.path.dirname(pwd)
    fallback_root = Dir('#').abspath # the SCons root
    return fallback_root


def _is_part_of_integrated_build():
    """
    Return true if it appears that we are part of an integrated build.

    """
    return os.path.exists(
        os.path.join(find_project_root(), 'patchbuild.py'))


def _get_default_dependency_location(dependency):
    global dependencyPolicy
    if not dependencyPolicyKnown:
        if _is_part_of_integrated_build():
            dependencyPolicy = 'checkedout'
        else:
            dependencyPolicy = 'fetched'

    if dependencyPolicy == 'checkedout':
        default_location = checked_out_locations[dependency]
    else:
        default_location = os.path.join(GetOption('csfDependencyDir'), dependency)
    return default_location


def _calculate_dependency_location(location):
    """
    Looks for a dependency, trying 
        1) abspath, 
        2) project root as specified

    """
    global dependencyPolicy
    locations = []
    if os.path.isabs(location):
        locations.append(location)
        
    if dependencyPolicy == 'checkedout':
        project_root = GetOption('projectRoot')
        from_project_root = os.path.join(project_root, location)
        from_project_root = os.path.abspath(from_project_root)
        locations.append(from_project_root)
    else: # fetched
        sconstruct_location = Dir('#').abspath
        locations.append(os.path.join(sconstruct_location, location))
    
    for loc in locations:
        if os.path.exists(loc):
            return loc
    raise DependencyException("Unable to find " + location) 

def _parse_chkdout_dep_file(filename):
    """ 
    Reads a local dependency file
    Returns a dictionary of component_name:component_location
    (Return value is also a global of this script)

    This is called with default values when module is imported

    """
    chkdoutDeps = {}
    with open(filename) as chkdout_locations:
        for line in chkdout_locations:
            line = line.strip()
            if not line.startswith('#') and len(line) != 0:
                try:
                    key,value = line.split()
                except (ValueError):
                    err_msg = ("Unable to parse " + filename + 
                               "at line: " + line)
                    sys.write.stderr(err_msg)
                    exit(1)
                chkdoutDeps[key] = value
        
    return chkdoutDeps

def processDependencies(dependencies, dependency_location, errorDependencies, SConscriptDirs):
    for depend in dependencies:
        # If the path is not absolute, we need to get the location relative to the SConstruct
        # To do this, we use the SCons Dir type which allows us to use the '#' prefix to 
        # specify that the path is relative to the SConstruct.
        # The side effect of this is we then need to use the str() method to be able to 
        # pass the paths to the os-type operations
        dependOption = 'csfDependencyDir_' + depend
        AddOption('--' + dependOption,
                type = 'string',
                dest = dependOption, # if dest not used, rejects options with a '-'
                action = 'store',
                default=_get_default_dependency_location(depend),
                 )
        Help("\tOption: --%s=<location>  (default:%s)\n" % 
                (dependOption, _get_default_dependency_location(depend)))

        dependDir = GetOption(dependOption)
        try:
            dependDir = _calculate_dependency_location(dependDir)
        except DependencyException:
            if not GetOption('help'):
                errorDependencies[depend] = dependDir
                continue
            
        dependency_location[depend] = dependDir
        if depend in ['scripts', 'jcfscripts']:
            scriptsDirs.append(dependDir)
            scriptsDirs.append(os.path.join(dependDir,'build'))
            if 'jcfscripts' == depend:
                scriptsDirs.append(os.path.join(dependDir,'shoggoth'))
        else:
            SConscriptDirs.append(dependDir)


Help("""

------ DEPENDENCY MANAGEMENT HELP -------

 Option: --useCheckedOutDependencies
    Use Checked Out Dependencies when using 'single repo'.
    Read from file specified by --checkedoutDependencyLocationFile
    Default -- OFF

 Option: --checkedoutDependencyLocationFile='filename'
    Specify the file that lists the dependency locations.

 Option: --projectRoot='dir/location'
    Specify the 'root' of dependencies listed in dependency location file.
    Default: directory containing 'patchbuild.py file (searches)

 Option: --csfDependencyDir
    The path to the directory containing the fetched dependencies. 
    Should be absolute, or relative to the SConstruct location.

""")

AddOption('--checkedoutDependencyLocationFile',
          type = 'string',
          action = 'store',
          default = 'CheckedOutDependencyLocation.txt',
         )
AddOption('--useCheckedOutDependencies',
          action = 'store_true',
          default = False,
         )
AddOption('--useFetchedDependencies',
          action = 'store_true',
          default = False,
          )
AddOption('--enableFips',
          dest='enableFips',
          action='store_true',
          default=False,
          help='Enable FIPS support in the process.'
          )
AddOption('--csfHttpTrace',
          dest='csfHttpTrace',
          action='store_true',
          default=False,
          help='Enable HTTP debug information in CSFNetUtils.'
          )

checked_out_locations = _parse_chkdout_dep_file(GetOption('checkedoutDependencyLocationFile'))

if GetOption('useCheckedOutDependencies'):
    dependencyPolicy = 'checkedout'
    dependencyPolicyKnown = True
elif GetOption('useFetchedDependencies'):  # Note only checked if useCheckedOutDep is False
    dependencyPolicy = 'fetched'
    dependencyPolicyKnown = True



AddOption('--csfDependencyDir',
          dest='csfDependencyDir',
          type='string',
          action='store',
          default='dependencies')

AddOption('--projectRoot', 
          type = 'string',
          action = 'store',
          default = find_project_root()
         )




# Set up a directory list for the SConscripts

SConscriptDirs = []
testSConscriptDirs = []
scriptsDirs = []
errorDependencies = {}


Help("""
To Specify the location of a dependency, use one of the following options:

     """)

dependency_location = {}

processDependencies(dependencies,dependency_location,errorDependencies, SConscriptDirs)
# process test dependencies if defined 
if (testDependencies is not None):
    processDependencies(testDependencies,dependency_location,errorDependencies, testSConscriptDirs)

# Second way to retrieve the dependency location. This approach allows the caller
# to iterate over the dependencies. i.e. Knowledge of the names is not a requirement.
AddMethod(Environment, lambda env: dependency_location, 'GetDependencyLocations')

dependency_err_advice = """

To fetch dependencies, you can use the [bootstrap/fetchDependencies.py] script.
Instructions to use this script are available from [bootstrap/README].

If you are using the 'single checkout', you may use the checked-out
 dependencies by specifying the option '--useCheckedOutDependencies'.

Alternatively, you can specify a location on disk for any dependency by 
 using the --csfDependencyDir_<dependency_name> option

 """

# Check if we have an error in here - if we do, we should report the failure, and exit
if 0 != len(errorDependencies):
    print >> sys.stderr, "\nERROR: The following dependencies have not been found."
    for key in errorDependencies.keys():
        print >> sys.stderr, "\t* %s - %s" %(key, errorDependencies[key])
    print >> sys.stderr, dependency_err_advice
    exit(-1)


Help("""
 ----------------------------------------------------------------------

""")
# Add an option to enable CacheDir()
AddOption('--csfCacheDir',
          dest='cache_directory',
          type='string',
          action='store',
          default=None)

if GetOption('cache_directory') != None:
    CacheDir(GetOption('cache_directory'))

# If test dependencies defined return test dirs as well, else just return normal dirs
if (testDependencies is None) :
  Return ('scriptsDirs', 'SConscriptDirs')
else:
  Return ('scriptsDirs', 'SConscriptDirs', 'testSConscriptDirs')
